--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit aa54d4f570aa87b0e58a20441e8e22adab650e94
Parents : 9d98656
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-06-21T16:16:51-05:00
feat(electron): implement window open handling for child Electron windows and external URLs
Changes
3 files changed, 75 insertions(+), 47 deletions(-)
Diff
diff --git a/electron/main.js b/electron/main.js
index 1ea171e8..aae18005 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -17,7 +17,12 @@ const fs = require("fs");
const path = require("node:path");
const { createBackendProcessManager } = require("./backendProcess");
-const { getUserProvidedArguments, formatRenderProcessGoneDetails, isLocalBackendUrl } = require("./mainHelpers");
+const {
+ getUserProvidedArguments,
+ formatRenderProcessGoneDetails,
+ isLocalBackendUrl,
+ shouldOpenInElectronWindow,
+} = require("./mainHelpers");
const { isAllowedShellPath } = require("./shellPathGuard");
const { normalizeExternalUrlForOpen } = require("./safeExternalUrl");
@@ -331,6 +336,40 @@ ipcMain.handle("pick-directory", async () => {
return filePaths[0];
});
+function getChildBrowserWindowOptions() {
+ return {
+ autoHideMenuBar: true,
+ webPreferences: {
+ preload: path.join(__dirname, "preload.js"),
+ nodeIntegration: false,
+ contextIsolation: true,
+ sandbox: true,
+ enableRemoteModule: false,
+ },
+ };
+}
+
+function handleWindowOpenRequest(url) {
+ if (shouldOpenInElectronWindow(url)) {
+ return {
+ action: "allow",
+ overrideBrowserWindowOptions: getChildBrowserWindowOptions(),
+ };
+ }
+
+ const safe = normalizeExternalUrlForOpen(url);
+ if (safe) {
+ shell.openExternal(safe);
+ }
+ return {
+ action: "deny",
+ };
+}
+
+function attachWindowOpenHandler(browserWindow) {
+ browserWindow.webContents.setWindowOpenHandler(({ url }) => handleWindowOpenRequest(url));
+}
+
function attachDevToolsF12Shortcut(browserWindow) {
browserWindow.webContents.on("before-input-event", (event, input) => {
if (input.type !== "keyDown" || input.key !== "F12") {
@@ -570,6 +609,7 @@ app.whenReady().then(async () => {
app.on("browser-window-created", (event, browserWindow) => {
attachDefaultContextMenu(browserWindow);
attachDevToolsF12Shortcut(browserWindow);
+ attachWindowOpenHandler(browserWindow);
});
// Security: Enforce CSP for all requests as a shell-level fallback
@@ -670,52 +710,6 @@ app.whenReady().then(async () => {
}
});
- // open external links in default web browser instead of electron
- mainWindow.webContents.setWindowOpenHandler(({ url }) => {
- var shouldShowInNewElectronWindow = false;
-
- // we want to open call.html in a new electron window
- // but all other target="_blank" links should open in the system web browser
- // we don't want /rnode-flasher/index.html to open in electron, otherwise user can't select usb devices...
- if (
- (url.startsWith("http://localhost") || url.startsWith("https://localhost")) &&
- url.includes("/call.html")
- ) {
- shouldShowInNewElectronWindow = true;
- }
-
- // we want to open blob urls in a new electron window
- else if (url.startsWith("blob:")) {
- shouldShowInNewElectronWindow = true;
- }
-
- // open in new electron window
- if (shouldShowInNewElectronWindow) {
- return {
- action: "allow",
- overrideBrowserWindowOptions: {
- autoHideMenuBar: true,
- webPreferences: {
- preload: path.join(__dirname, "preload.js"),
- nodeIntegration: false,
- contextIsolation: true,
- sandbox: true,
- enableRemoteModule: false,
- },
- },
- };
- }
-
- // fallback to opening any other url in external browser (http(s) / mailto only)
- const safe = normalizeExternalUrlForOpen(url);
- if (safe) {
- shell.openExternal(safe);
- }
- return {
- action: "deny",
- };
- });
-
// navigate to loading page
await mainWindow.loadFile(path.join(__dirname, "loading.html"));
diff --git a/electron/mainHelpers.js b/electron/mainHelpers.js
index 94f239ea..0c76c0cb 100644
--- a/electron/mainHelpers.js
+++ b/electron/mainHelpers.js
@@ -47,8 +47,31 @@ function isLocalBackendUrl(url) {
);
}
+/**
+ * Whether window.open should create a child Electron window instead of the OS browser.
+ * Local backend popouts and call.html must stay in Electron so they keep the app session.
+ * @param {unknown} url
+ * @returns {boolean}
+ */
+function shouldOpenInElectronWindow(url) {
+ if (!url || typeof url !== "string") {
+ return false;
+ }
+ if (url.startsWith("blob:")) {
+ return true;
+ }
+ if (!isLocalBackendUrl(url)) {
+ return false;
+ }
+ if (url.includes("/call.html")) {
+ return true;
+ }
+ return url.includes("#/popout/");
+}
+
module.exports = {
getUserProvidedArguments,
formatRenderProcessGoneDetails,
isLocalBackendUrl,
+ shouldOpenInElectronWindow,
};
diff --git a/tests/electron/mainHelpers.test.js b/tests/electron/mainHelpers.test.js
index df214ed4..deb6d67f 100644
--- a/tests/electron/mainHelpers.test.js
+++ b/tests/electron/mainHelpers.test.js
@@ -6,6 +6,7 @@ const {
getUserProvidedArguments,
formatRenderProcessGoneDetails,
isLocalBackendUrl,
+ shouldOpenInElectronWindow,
} = require("../../electron/mainHelpers.js");
describe("electron/mainHelpers", () => {
@@ -31,4 +32,14 @@ describe("electron/mainHelpers", () => {
expect(isLocalBackendUrl("https://example.com")).toBe(false);
expect(isLocalBackendUrl("")).toBe(false);
});
+
+ it("shouldOpenInElectronWindow keeps local popouts and call windows in Electron", () => {
+ expect(shouldOpenInElectronWindow("https://127.0.0.1:9337/#/popout/map")).toBe(true);
+ expect(shouldOpenInElectronWindow("https://127.0.0.1:9337/call.html")).toBe(true);
+ expect(shouldOpenInElectronWindow("http://localhost:9337/#/popout/messages/abc")).toBe(true);
+ expect(shouldOpenInElectronWindow("blob:https://127.0.0.1:9337/print")).toBe(true);
+ expect(shouldOpenInElectronWindow("https://127.0.0.1:9337/rnode-flasher/index.html")).toBe(false);
+ expect(shouldOpenInElectronWindow("https://example.com/#/popout/map")).toBe(false);
+ expect(shouldOpenInElectronWindow("")).toBe(false);
+ });
});
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────